﻿namespace SpikingAnalyticsLib

open System.IO
open System.Collections.Generic
open SpikingNeuronLib
open SpikingAnalyticsLib
open SpikingAnalyticsLib.GeneralUtilities
open SpikingAnalyticsLib.PatternUtilities
open SpikingAnalyticsLib.NetworkUtilities
open SpikingAnalyticsLib.GraphUtilities

// ===============================================================================================================
// PNGDescriptorSet: a type that holds a related set of PNGDescriptors

type PNGDescriptorSet(descriptors:PNGDescriptor list) =

    /// Load a descriptor set from the specified file path
    new(loadPath) =
        let descriptorList =
            File.ReadLines(loadPath)
            |> Seq.fold (fun (state:string list list) line ->
                    if line.Length > 0 then
                        (line :: state.Head) :: state.Tail
                    else
                        [] :: state
                ) [[]]
            |> Seq.filter (fun lines -> lines.Length > 0)
            |> Seq.map (fun lines ->
                    let reversedLines = List.rev lines
                    new SpikingAnalyticsLib.PNGDescriptor(reversedLines)
                )
            |> Seq.toList
            
        new PNGDescriptorSet(descriptorList)

    /// Create a descriptor set from the specified list of templates
    /// by using the connectivity data in the specified Izhikevich network
    new(templateList:TemplateList, network) =
        let postNeuronMap = GetPostNeuronMap network
        let descriptorList =
            templateList.Patterns
            |> Seq.map (fun template ->
                    let groupEvents = Seq.map (fun (evt:FiringEvent) -> evt.Time, evt.NeuronIndex, 1.0) template.EventList   // default link probability of 1.0
                    let jitter = 2      // was network.MaximumAxonalDelay
                    let minTime, maxTime, linkedEventsList = GetGroupLinksFromPostNeuronMap jitter postNeuronMap groupEvents
                    PNGDescriptor(minTime, maxTime, linkedEventsList)
                )
            |> Seq.toList
        new PNGDescriptorSet(descriptorList)

    /// Create a descriptor set from the specified linkage data
    new(linkageData:seq<LinkedEventPair list>) =
        let descriptorList =
            linkageData
            |> Seq.map (fun groupLinkedEvents -> PNGDescriptor(groupLinkedEvents))
            |> Seq.toList
        new PNGDescriptorSet(descriptorList)

    /// Create a descriptor set from the specified map of source file index to Pattern
    /// by using the linkage data in the specified PNG groups file
    new(matchPatterns, groupsFilePath) =
        let descriptorList =
            GetIndexedPNGTemplateLinkData groupsFilePath matchPatterns
            |> Seq.map (fun groupLinkedEvents -> PNGDescriptor(groupLinkedEvents))
            |> Seq.toList
        new PNGDescriptorSet(descriptorList)

    member this.Descriptors = descriptors

    member this.Save(outputPath:PathDescriptor) =
        use sw = File.CreateText(outputPath.Path("txt"))
        descriptors
        |> Seq.iter (fun descriptor ->
                let header = sprintf "%d %d" descriptor.FirstFiringTime descriptor.LastFiringTime
                sw.WriteLine(header)
                descriptor.LinkedEventsAsTuples
                |> Seq.iter (fun ((time1, nindex1), (time2, nindex2), weight, probability) ->
                        let line = sprintf "%d %d %d %d %f %f" time1 nindex1 time2 nindex2 weight probability
                        sw.WriteLine(line)
                    )
                sw.WriteLine()  // blank line between descriptors
            )

// ===============================================================================================================
// PatternDescriptor: describes a single pattern

/// A minimal description of a single pattern
[<AllowNullLiteral>]
type PatternDescriptor(firingEvents:List<FiringEvent>) =

    new (firingEvents:seq<FiringEvent>) = PatternDescriptor(new List<FiringEvent>(firingEvents))

    member this.EventData = firingEvents

// ===============================================================================================================
// PatternList: a list of firing patterns (not input patterns as there's no repetition defined)
// A convenience type for use with the VanillaAPI
// Wraps a pattern list to avoid exposing FSharp types

[<AllowNullLiteral>]
[<System.Reflection.DefaultMember("Item")>]
type PatternList(firingPatterns:(int * int) list option list) =

    let ConvertToPatternList patterns =
        let patternList =
            patterns
            |> Seq.map (fun (pattern:#seq<int * int> option) ->
                    if pattern.IsSome then
                        let firingEventList =
                            pattern.Value
                            |> Seq.map (fun (time, nindex) -> new FiringEvent(time, nindex, EventLabel.Unspecified))
                        new PatternDescriptor(firingEventList)
                    else
                        null
                )
        new List<PatternDescriptor>(patternList)

    // create a list of multiple patterns
    new(firingEventLists:FiringEvent list option list) =
        let patterns =
            firingEventLists
            |> Seq.map (fun firingEventList ->
                    if firingEventList.IsSome then
                        let pattern =
                            firingEventList.Value
                            |> Seq.map (fun evt -> evt.Time, evt.NeuronIndex)
                            |> Seq.toList
                        Some(pattern)
                    else
                        None
                )
            |> Seq.toList
        new PatternList(patterns)

    // create a list composed of one pattern
    new(firingEvents:FiringEvent list) =
        new PatternList([ Some(firingEvents) ])

    member this.Item
        with get(index) = firingPatterns.[index]

    member this.Patterns = firingPatterns

    member this.Length = firingPatterns.Length

    member this.PatternList = ConvertToPatternList firingPatterns

    static member Create(patterns:List<PatternDescriptor>) =
        let ConvertToInternalPatternList (patterns:List<PatternDescriptor>) =
            patterns
            |> Seq.map (fun firingEventList ->
                    if firingEventList = null then
                        None
                    else
                        let pattern =
                            firingEventList.EventData
                            |> Seq.map (fun evt -> evt.Time, evt.NeuronIndex)
                            |> Seq.toList
                        Some(pattern)
                )
            |> Seq.toList

        PatternList(ConvertToInternalPatternList patterns)

    /// Combine two or more patterns (taken in sets of two)
    /// to create a new list of merged patterns
    static member CombinePatterns(patterns:PatternList) =
        let combinedPatterns = CombinePatterns patterns.Patterns
        new PatternList(combinedPatterns)

// ===============================================================================================================
// ResponseProfile: wraps the profile maps from a generated response profile

type ResponseProfile(responseProfileMap:Map<int, Map<int, int>>) =

    // Save the mapping between neurons and their reponse histograms
    // where the response histogram is a map of spike counts keyed by temporal offset from an input pattern neuron
    let SaveResponseProfileMap (savePath:string) allMaps =
        let GetMaps (allMaps:Map<int, Map<int, int>>) =
            let StringifyMap map =
                map
                |> Map.toSeq
                |> Seq.map (fun (key, data) ->
                        sprintf "%d" data           // key value is implied by line position in the file
                    )
                |> String.concat " "
            allMaps
            |> Map.toSeq
            |> Seq.map (fun (key, dataMap) ->
                    let data = StringifyMap dataMap
                    sprintf "%d %s" key data
                )
        use sw = File.CreateText(savePath)
        GetMaps allMaps
        |> Seq.iter (fun line -> sw.WriteLine(line))    

    // Find the maximum spike count for all of the response profiles of the selected neurons
    // Useful for setting a maximum Y-value for spike profile charts
    let GetMaxSpikeCount allMaps selectedNeurons =
        let selectedSet = Set.ofList selectedNeurons
        allMaps
        |> Map.filter (fun neuronIndex profile -> selectedSet.Contains neuronIndex)
        |> Map.fold (fun state neuronIndex profile ->
                let maxOffset, maxCount =
                    profile
                    |> Map.toSeq
                    |> Seq.maxBy (fun (offset, spikeCount) -> spikeCount)
                if maxCount > state then
                    maxCount
                else
                    state
            ) 0

    member this.InternalMap = responseProfileMap

    member this.Data =
        ConvertMappedValues ConvertMapToDictionary responseProfileMap
        |> ConvertMapToDictionary

    /// Save the mapping between neurons and their reponse histograms
    member this.Save(savePath) = SaveResponseProfileMap savePath responseProfileMap

    /// Save the mapping between neurons and their reponse histograms
    member this.Save(savePathDescriptor:PathDescriptor) =
        let path =
            let folderPath, fileBase = savePathDescriptor.Folder, savePathDescriptor.FileBaseName
            Path.Combine(folderPath, sprintf "%s.txt" fileBase)
        SaveResponseProfileMap path responseProfileMap

    /// Find the maximum spike count for all of the response profiles of the selected neurons
    member this.GetMaxProfileSpikeCount(selectedNeurons) =
        GetMaxSpikeCount responseProfileMap selectedNeurons

// ===============================================================================================================
// ResponseGrid: a type that generates a peak response value for each combination of timing delays for two input neurons
// The outer map is keyed by a two integer int list which is the timing delays for the two input neurons
// The inner map is keyed by neuron index (of the responding neuron)
// and has the values: peakTime and peakCount
type ResponseGrid(header:string, response:Map<int list, Map<int, int * int>>) =

    let GetResponsePatternString responsePattern =
        responsePattern
        |> Map.toSeq
        |> Seq.map (fun (neuronIndex, (peakTime, peakCount)) -> peakCount)
        |> StringifyWithSeparator " "

    let ShowResponsePatterns patterns =
        patterns
        |> Map.toSeq
        |> Seq.iter (fun (delayList, responsePattern) ->
                let delayString = StringifyWithSeparator " " delayList
                let responseString = GetResponsePatternString responsePattern
                printfn "%s: %s" delayString responseString
            )

    // Save the mapping between input patterns and peak response patterns
    // The 'header' argument is for saving arbitrary additional data in the same file
    // Only the peakCount is saved (the neuron index is inferred from position and the peakTime is not needed)
    let SaveResponsePatterns (savePath:PathDescriptor) (header:string) patterns =
        let GetResponses patterns =
            patterns
            |> Map.toSeq
            |> Seq.map (fun (delayList, responsePattern) ->
                    let delayString = StringifyWithSeparator " " delayList
                    let responseString = GetResponsePatternString responsePattern
                    sprintf "%s %s" delayString responseString
                )
        use sw = File.CreateText(savePath.Path("txt"))
        sw.WriteLine(header)
        GetResponses patterns
        |> Seq.iter (fun line -> sw.WriteLine(line))    

    member this.WriteToConsole() = ShowResponsePatterns response

    member this.SaveResponse(savePath:PathDescriptor) = SaveResponsePatterns savePath header response

// ===============================================================================================================
// HeuristicResponseGrid: a type that generates a list of responding neurons for each combination of timing delays for two input neurons
// The map is keyed by a two integer int list which is the timing delays for the two input neurons
// The value is a list of responding neurons
type HeuristicResponseGrid(header:string, outputNeurons, response:Map<int list, int list>) =

    // Create a bit pattern with a 1 flag set for every index in 'selected'
    let CreateBitPattern selected count =
        let result = [| for i in 1..count do yield 0 |]
        selected
        |> Seq.iter (fun index -> result.[index] <- 1)
        result

    let ShowResponsePatterns patterns =
        patterns
        |> Map.toSeq
        |> Seq.iter (fun (delayList, respondingNeurons) ->
                let delayString = StringifyWithSeparator " " delayList
                let responseString =
                    CreateBitPattern respondingNeurons outputNeurons
                    |> StringifyWithSeparator " "
                printfn "%s: %s" delayString responseString
            )

    // Save the mapping between input patterns and peak response patterns
    // The 'header' argument is for saving arbitrary additional data in the same file
    // Only the peakCount is saved (the neuron index is inferred from position and the peakTime is not needed)
    let SaveResponsePatterns (savePath:PathDescriptor) (header:string) patterns =
        let GetResponses patterns =
            patterns
            |> Map.toSeq
            |> Seq.map (fun (delayList, respondingNeurons) ->
                    let delayString = StringifyWithSeparator " " delayList
                    let responseString =
                        CreateBitPattern respondingNeurons outputNeurons
                        |> StringifyWithSeparator " "
                    sprintf "%s %s" delayString responseString
                )
        use sw = File.CreateText(savePath.Path("txt"))
        sw.WriteLine(header)
        GetResponses patterns
        |> Seq.iter (fun line -> sw.WriteLine(line))    

    member this.WriteToConsole() = ShowResponsePatterns response

    member this.SaveResponse(savePath:PathDescriptor) = SaveResponsePatterns savePath header response

// ===============================================================================================================
// HighResolutionDataCollector: a high-temporal resolution (one millisecond) collector of membrane and synaptic weight data
// Deprecated: see OneMillisecTickDataCollector (GeneralTypes) instead

[<AllowNullLiteral>]
type HighResolutionDataCollector(selectedNeurons:int [], selectedConnections:int [], totalNeurons, totalConnections, numberOfMembraneSamples, numberOfWeightSamples) =

    let SaveToFile (savePath:PathDescriptor) data =
        use sw = File.CreateText(savePath.Path("txt"))
        data
        |> Array2D.iteri (fun row col (v, u) ->
                let line = sprintf "%d %f %f" row v u
                sw.WriteLine(line)
            )

    let selectedNeuronsList = List.ofSeq selectedNeurons
    //let membraneDataParameters = selectedNeuronsList, numberOfMembraneSamples, totalNeurons

    let selectedConnectionsList = List.ofSeq selectedConnections
    //let weightDataParameters = selectedConnectionsList, numberOfWeightSamples, totalConnections

    let parameters = new OneMillisecTickDataCollectorParameters(selectedNeuronsList, totalNeurons, selectedConnectionsList, totalConnections, numberOfMembraneSamples, numberOfWeightSamples)

    let collector = OneMillisecTickDataCollector(parameters)

    member internal this.Collector = collector

    member this.MembraneDataRaw =
        collector.MembraneData

    member this.SynapticWeightDataRaw =
        collector.ConnectionDataPairs.Value

    member this.FirstNMembraneSamplesRaw(n) =
        collector.FirstNMembraneSamples n

    member this.FirstNSynapticWeightSamplesRaw(n) =
        (collector.FirstNConnectionSamples n).Value

    member this.MembraneData =
        collector.MembraneDataPairs.Value
        |> Array2D.map (fun (v, u) -> MembraneDataPoint(v, u))

    member this.SynapticWeightData =
        collector.ConnectionDataPairs.Value
        |> Array2D.map (fun (derivative, weight) -> SynapticWeightDataPoint(derivative, weight))

    /// Save the membrane data to the specified path
    member this.SaveMembraneData(savePath:PathDescriptor) =
        if savePath = null then
            failwith "Invalid path descriptor"
        SaveToFile savePath collector.MembraneDataPairs.Value

    /// Save the synaptic data to the specified path
    member this.SaveSynapticData(savePath:PathDescriptor) =
        if savePath = null then
            failwith "Invalid path descriptor"
        SaveToFile savePath collector.ConnectionDataPairs.Value

// ===============================================================================================================
// LowResolutionDataCollector: a low-temporal resolution (one second) collector of firing event data
// Deprecated: see OneSecondTickDataCollector (GeneralTypes) instead

[<AllowNullLiteral>]
type LowResolutionDataCollector(firingEvents:seq<int * int * EventLabel>) =

    let SaveToFile (savePath:PathDescriptor) firingEvents =
        use sw = File.CreateText(savePath.Path("txt"))
        firingEvents
        |> Seq.iter (fun (time, nindex, label) ->
                let line = sprintf "%d %d %s" time nindex (System.Enum.GetName(label.GetType(), label))
                sw.WriteLine(line)
            )

    let collector = OneSecondTickDataCollector(firingEvents)

    new() = new LowResolutionDataCollector([])

    member internal this.Collector = collector

    /// Return the number of firing events collected
    member this.Count = collector.FiringEvents.Count

    /// Return all firing events as a sequence of (time, nindex) pairs
    member this.AllEventPairs = collector.AllEventPairs

    /// Return firing events not marked as background events as a sequence of (time, nindex) pairs
    member this.ForegroundEventPairs = collector.ForegroundEventPairs

    /// Return all firing events as a sequence of (time, nindex, isBackgroundEvent) triplets
    member this.AllEventTriplets = collector.AllEventTriplets

    /// Return firing events not marked as background events as a sequence of (time, nindex, isBackgroundEvent) triplets
    member this.ForegroundEventTriplets = collector.ForegroundEventTriplets

    /// Return all firing events as a list of FiringEvent items
    member this.AllFiringEvents = collector.AllFiringEvents

    /// Return a OneSecondTickDataCollector containing the selected data
    member this.SelectRange(minTime, maxTime, selectForegroundEventsOnly) = collector.SelectRange(minTime, maxTime, selectForegroundEventsOnly)

    /// Save firing data to the specified path
    member this.Save(savePath:PathDescriptor) =
        if savePath = null then
            failwith "Invalid path descriptor"
        SaveToFile savePath collector.FiringEvents

    /// Clear any firing data
    member this.Clear() = collector.Clear()

    /// Return a new LowResolutionDataCollector containing data that has been filtered using the Pulse Discovery data in the specified path
    static member ApplyFilter(collector:OneSecondTickDataCollector, dataPath, jitter) =
        let filter = LoadMapPrePost dataPath
        let filteredData = FilterEvents jitter filter collector.ForegroundEventTriplets
        new OneSecondTickDataCollector(filteredData)

    /// Return a new LowResolutionDataCollector containing data that has been filtered using network connectivity data
    static member ApplyFilter(collector:OneSecondTickDataCollector, spn:CrossbarNetwork, jitter, minWeight) =
        let filter = GetPairDataFromNetworkStructure spn minWeight
        let filteredData = FilterEvents jitter filter collector.ForegroundEventTriplets
        new OneSecondTickDataCollector(filteredData)

// ===============================================================================================================
// PeakCollector: a type for collecting the peaks in the response profiles for each pulsed neuron
// Pulse Discovery

type PeakCollector(size:int, inputPeriod, thresholdScale) =

    let store = new Dictionary<int, (int * int * int) list>(size);
    let selectedNeurons = [for i in 0..size-1 -> i]

    member this.AsMap
        with get() =
            seq {
                for kvp in store do
                    yield kvp.Key, kvp.Value
            }
            |> Map.ofSeq

    member this.AddProfile(pulsedNeuron, loResDataCollector:OneSecondTickDataCollector) =
        // Select the peak counts (and corresponding times) for each profiles (Pulse Discovery)
        let GetPeakProfiles pulsedNeuron totalNeurons inputPeriod thresholdScale selectedNeurons selectedFiringEvents =

            // Copied to resolve dependancies: see FrameProfile
            /// Generate a map between selected neurons and their reponse histograms
            /// where the response histogram is a map of spike counts keyed by temporal offset from a specified frame
            let GenerateFrameProfiles frameSize totalNeurons selectedNeurons firingEventList =

                let selectionMask = CreateFilter selectedNeurons totalNeurons

                firingEventList
                |> Seq.filter (fun (time, nindex, isBackground) -> selectionMask.[nindex])        // process only selected neurons
                |> Seq.map (fun (time, nindex, isBackground) -> time % frameSize, nindex)       // remap time into each frame of size 'frameSize'
                |> Seq.fold (fun (dict:Map<int,int list>) (time, nindex) ->         // a list of reponse times for each neuron
                        let times = dict.[nindex]
                        Map.add nindex (time :: times) dict
                    ) (PreAllocateMap totalNeurons [])      // prellocate the map to ensure that all neurons have a slot (even when not present in the data)
                |> Map.map (fun nindex times ->                                     // turn each list of response times into a separate map of counts keyed by time
                        times
                        |> Seq.fold (fun (dict:Map<int,int>) time ->
                                if dict.ContainsKey(time) then
                                    let count = dict.[time]
                                    Map.add time (count + 1) dict
                                else
                                    printfn "Warning: response time (%d) was out of range (%d time slots allocated)" time dict.Count
                                    dict
                            ) (PreAllocateMap frameSize 0)
                    )

            let result = GenerateFrameProfiles inputPeriod totalNeurons selectedNeurons selectedFiringEvents
            let peaksMap = QuantifyResponseProfiles result

            // get the threshold as a scalar value of the mode of the peak counts for these profiles
            let threshold =
                let peakCountMode, numInstances =
                    peaksMap
                    |> Map.toSeq
                    |> Seq.map (fun (neuron, (peakTime, peakCount)) -> peakCount, peakCount)
                    |> CreateKeyCountMap
                    |> Map.toSeq
                    |> Seq.maxBy (fun (peakCount, numInstances) -> numInstances)
                if peakCountMode = 0 then
                    thresholdScale
                else
                    thresholdScale * peakCountMode

            let selectedResponses =
                peaksMap
                |> Map.toSeq
                |> Seq.filter (fun (neuron, (_, peakCount)) -> (neuron <> pulsedNeuron) && peakCount >= threshold)
                |> Seq.map (fun (neuron, (peakTime, peakCount)) ->
                        neuron, peakTime, peakCount
                    )
                |> Seq.toList

            selectedResponses

        let peakProfiles = GetPeakProfiles pulsedNeuron size inputPeriod thresholdScale selectedNeurons loResDataCollector.ForegroundEventTriplets
        store.Add(pulsedNeuron, peakProfiles)

    member this.Save(outputPath:PathDescriptor) =
        use sw = File.CreateText(outputPath.Path("txt"))
        for kvp in store do
            fprintfn sw "%d" kvp.Key
            kvp.Value
            |> Seq.iter (fun (neuron, peakTime, peakCount) -> fprintfn sw "%d %d %d" neuron peakTime peakCount)

    member this.Load(inputPath) =
        store.Clear()
        LoadMapPrePost inputPath
        |> Map.iter (fun pulsedNeuron peakProfiles -> store.Add(pulsedNeuron, peakProfiles))

// ===============================================================================================================
// PNGActivationMatchData: a type for storing base match data read from a file produced by a PNG activation scan
// outer key:sampleTime
// inner key: templateID
// values: index, offset, matchCount, matchPattern

type PNGActivationMatchData(data:Map<int, Map<int, (int * int * int * string) list>>) =

    let GetMatchCountsPerSampleTime (data:Map<int, Map<int, (int * int * int * string) list>>) = 
        data
        |> Map.toSeq
        |> Seq.map (fun (sampleTime, templateMap) ->
                let matchCount =
                    templateMap
                    |> Map.toSeq
                    |> Seq.map (fun (templateID, matchDataList) -> matchDataList.Length)
                    |> Seq.sum
                sampleTime, matchCount
            )

    let GetTemplateCountsPerSampleTime (data:Map<int, Map<int, (int * int * int * string) list>>) = 
        data
        |> Map.toSeq
        |> Seq.map (fun (sampleTime, templateMap) ->
                let templateCount = templateMap.Count
                sampleTime, templateCount
            )

    let MakeDictionary data =
        data
        |> Seq.fold (fun (state:Dictionary<int, int>) (sampleTime, count) ->
                state.Add(sampleTime, count)
                state
            ) (new Dictionary<int, int>())

    /// Return the internal data as a map
    member this.InternalData = data

    /// Return a count of all matches keyed by sample time (counts each match of a template separately)
    member this.MatchCountsPerSampleTime =
        GetMatchCountsPerSampleTime data
        |> MakeDictionary

    /// Return a count of all matches keyed by sample time (counts each match of a template separately)
    member this.TemplateCountsPerSampleTime =
        GetTemplateCountsPerSampleTime data
        |> MakeDictionary

    member this.Save(outputPath:PathDescriptor) =
        use sw = File.CreateText(outputPath.Path("txt"))
        for kvpOuter in data do
            let sampleTime = kvpOuter.Key
            for kvpInner in kvpOuter.Value do
                let templateID = kvpInner.Key
                kvpInner.Value
                |> Seq.iter (fun (index, offset, matchCount, matchPattern) ->
                        fprintfn sw "%d %d %d %d %d %s" sampleTime templateID index offset matchCount matchPattern
                    )

    member this.SaveMatchCountsPerSampleTime(outputPath:PathDescriptor) =
        use sw = File.CreateText(outputPath.Path("txt"))
        GetMatchCountsPerSampleTime data
        |> Seq.iter (fun (sampleTime, matchCount) ->
                fprintfn sw "%d %d" sampleTime matchCount
            )

    member this.SaveTemplateCountsPerSampleTime(outputPath:PathDescriptor) =
        use sw = File.CreateText(outputPath.Path("txt"))
        GetTemplateCountsPerSampleTime data
        |> Seq.iter (fun (sampleTime, matchCount) ->
                fprintfn sw "%d %d" sampleTime matchCount
            )

// ===============================================================================================================
// PNGActivationMatchingTemplates: a type for storing matching template data read from a file produced by a PNG activation scan
// i.e. a match file (prior to introduction of the UniqueID; templateID only)
// argument: sampleTime: the time for which template matches were collected
// argument: data:
//      key: templateID
//      values: index, offset, matchCount, matchPattern, template

type PNGActivationMatchingTemplates(sampleTime, data:Map<int, (int * int * int * string * Pattern) list>) =

    // Get the first template for each key (where key is templateID and uniqueID)
    // Assumes that all templates are the same for each key
    let GetTemplateList data =
        let templates =
            data
            |> Map.toSeq
            |> Seq.map (fun (key, matchList) -> Seq.head matchList)
            |> Seq.map (fun (_, _, _, _, template) -> template)
            |> Seq.toArray
        new TemplateList(templates)

    /// Save all data (both base data and templates) for this sample time
    member this.SaveAll(outputPath:PathDescriptor) =
        use sw = File.CreateText(outputPath.Path("txt"))
        for kvp in data do
            let templateID = kvp.Key
            kvp.Value
            |> Seq.iter (fun (index, offset, matchCount, matchPattern, template) ->
                    fprintfn sw "%d %d %d %d %d %s" sampleTime templateID index offset matchCount matchPattern
                    fprintfn sw "%s" (template.ToString())
                )

    /// Save either all templates or just distinct templates for this sample time (base data is not saved)
    member this.Save(distinctOnly, outputPath:PathDescriptor) =
        use sw = File.CreateText(outputPath.Path("txt"))
        let templateList =
            let templates = GetTemplateList data
            if distinctOnly then
                templates.DistinctPatterns
            else
                templates
        for template in templateList.Patterns do
            fprintfn sw "%s" (template.ToString())

    /// Using connectivity data from the specified network, create a PNGDescriptorSet for this sample time
    /// that describes the structure of each distinct template
    member this.CreatePNGDescriptorSet(network:IzhikevichNetwork) =
        let templateList =
            let templates = GetTemplateList data
            templates.DistinctPatterns
        new PNGDescriptorSet(templateList, network)

    /// Using linkage data from the specified PNG groups file, create a PNGDescriptorSet for this sample time
    /// that describes the structure of each distinct template
    member this.CreatePNGDescriptorSet(groupsFilePath:string) =
        let templateIDToSourceMap = GetTemplateIDToSourceIndexMap groupsFilePath

        // create map of templates keyed by source file index
        let matchPatterns =
            data
            |> Map.toSeq
            |> Seq.map (fun (templateID, matchList) -> templateID, Seq.head matchList)
            |> Seq.map (fun (templateID, (_, _, _, _, template)) -> templateIDToSourceMap.[templateID], template)
            |> Map.ofSeq

        new PNGDescriptorSet(matchPatterns, groupsFilePath)

// ===============================================================================================================
// PNGActivationMatchingHashes: a type for storing matching template data read from a file produced by a PNG activation scan
// i.e. a match file with a UniqueID (pattern hash)
// argument: sampleTime: the time for which template matches were collected
// argument: data:
//      key: templateID, uniqueID
//      values: index, offset, matchCount, matchPattern, template

type PNGActivationMatchingHashes(sampleTime, data:Map<int * int, (int * int * int * string * Pattern) list>) =

    // Get the first template for each key (where key is templateID and uniqueID)
    // Assumes that all templates are the same for each key
    let GetTemplateList data =
        let templates =
            data
            |> Map.toSeq
            |> Seq.map (fun (key, matchList) -> Seq.head matchList)
            |> Seq.map (fun (_, _, _, _, template) -> template)
            |> Seq.toArray
        new TemplateList(templates)

    // Get the source template that matches the specified matching template
    // Note that if the matching template is a truncated version of the source template, this is still a match
    // If multiple matches are found, only one is returned (the rest are assumed to be repeats of the same template)
    let GetMatchingTemplates uniqueID (matchingTemplate:Pattern) (sourceTemplates:(int * Pattern) list) =
        let matches =
            seq {
                for (sourceIndex, sourceTemplate) in sourceTemplates do
                    // check if all firing events match along the length of the shorted template
                    let matched = Seq.forall2 (fun evt1 evt2 -> evt1.Equals(evt2)) sourceTemplate.EventList matchingTemplate.EventList
                    if matched then
                        yield sourceIndex, sourceTemplate
            }
            |> Seq.toList
        if matches.Length = 0 then
            failwith (sprintf "Failed to match pattern: %d %s" uniqueID (matchingTemplate.ToString()))
        else
            if matches.Length > 1 then
                printfn "WARNING: matching pattern found multiple source file matches (uniqueID = %d)" uniqueID
            matches.Head

    /// Save all data (both base data and templates) for this sample time
    member this.SaveAll(outputPath:PathDescriptor) =
        use sw = File.CreateText(outputPath.Path("txt"))
        for kvp in data do
            let templateID, uniqueID = kvp.Key
            kvp.Value
            |> Seq.iter (fun (index, offset, matchCount, matchPattern, template) ->
                    fprintfn sw "%d %d %d %d %d %d %s" sampleTime templateID uniqueID index offset matchCount matchPattern
                    fprintfn sw "%s" (template.ToString())
                )

    /// Save either all templates or just distinct templates for this sample time (base data is not saved)
    member this.Save(distinctOnly, outputPath:PathDescriptor) =
        use sw = File.CreateText(outputPath.Path("txt"))
        let templateList =
            let templates = GetTemplateList data
            if distinctOnly then
                templates.DistinctPatterns
            else
                templates
        for template in templateList.Patterns do
            fprintfn sw "%s" (template.ToString())

    /// Using connectivity data from the specified network, create a PNGDescriptorSet for this sample time
    /// that describes the structure of each distinct template
    member this.CreatePNGDescriptorSet(network:IzhikevichNetwork) =
        let templateList =
            let templates = GetTemplateList data
            templates.DistinctPatterns
        new PNGDescriptorSet(templateList, network)

    /// Using linkage data from the specified PNG groups file, create a PNGDescriptorSet for this sample time
    /// that describes the structure of each distinct template
    member this.CreatePNGDescriptorSet(groupsFilePath:string) =

        let allTemplatesTimeSourceIndexMap = PreparePNGTemplatesDictionary groupsFilePath None
        let allTemplatesHashSourceIndexMap = GetTemplateHashMap allTemplatesTimeSourceIndexMap

        // create map of templates keyed by source file index
        let matchPatterns =
            data
            |> Map.toSeq
            |> Seq.map (fun ((_, uniqueID), matchList) -> uniqueID, Seq.head matchList)
            |> Seq.map (fun (uniqueID, (_, _, _, _, template)) ->
                    if allTemplatesHashSourceIndexMap.ContainsKey uniqueID then
                        // get the source index for the source pattern that is a superset of the match pattern
                        GetMatchingTemplates uniqueID template allTemplatesHashSourceIndexMap.[uniqueID]
                    else
                        failwith (sprintf "UniqueID not found: %d" uniqueID)
                )
            |> Map.ofSeq

        new PNGDescriptorSet(matchPatterns, groupsFilePath)
